#!/usr/bin/python2
# -*- coding:utf-8 -*-

##  Fourtou - Store text or uri by DnD on an icon in the GNOME panel
##  Copyright (C) 2009-2010   Bzoloid
##
##  This program is free software; you can redistribute it and/or
##  modify it under the terms of the GNU General Public License
##  as published by the Free Software Foundation; either version 2
##  of the License, or (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program; if not, write to the Free Software Foundation,
##  Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

#------------------------------------------------------------------------------


"""Fourtou applet for the GNOME panel."""

__version__ = '0.4.0'
__date__    = '2011-04-09'
__author__  = 'Bzoloid <bzoloid@gmail.com>'
__licence__ = 'GNU GPL v2'

import subprocess
import sqlite3
import time
import sys
import os.path
DATADIR = "./"

import glib
import pygtk
pygtk.require('2.0')
import gtk
import pango



#===# CLASSES #================================================================


class Fourtou:
    """Main class of the programm."""
    
    def __init__(self, parent, iid=None):
        self.parent = parent
        self.data = FTData()
        self.win = FTWindow(self)
        self.icon = FTIcon(self)
        self.parent.show_all()
    
    def get_date(self):
        """Format the current date."""
        return int("%d%02d%02d" % time.localtime()[:3])
    
    def send_notif(self, text, icon=None, urgency=None):
        """Display a notification bubble."""
        title = "Fourtou"
        i = ["-i", icon] if icon else []
        u = ["-u", urgency] if urgency else []
        try:
            p = subprocess.Popen(["notify-send","-t","3000"]+u+i+[title,text])
            p.wait()
        except:
            pass
        return



#------------------------------------------------------------------------------


class FTData():
    """Interface for reading/writing data."""
    
    def __init__(self):
        self.filename = os.path.join(glib.get_user_config_dir(), "fourtou.db")
        self.modes = {'i':"_init", 'r':"_read", 'w':"_write",
                      'u':"_update", 'd':"_delete", 'c':"_clean"}
        self.cnx = None
        if self.open('i') is None:
            sys.exit(1)
    
    def open(self, mode, data=None):
        """Connect to the database."""
        if mode not in self.modes:
            return None
        try:
            func = getattr(self, self.modes[mode])
            ret = func(data)
            self.cnx.commit()
        except:
            print "Fourtou > DB error!"
            ret = None
        return ret
    
    def _init(self, data):
        """Initialise the database."""
        test = os.path.exists(self.filename)
        self.cnx = sqlite3.connect(self.filename)
        self.cnx.text_factory = str
        self.cur = self.cnx.cursor()
        if not test:
            self.cur.execute('PRAGMA encoding="UTF-8"')
            self.cur.execute(
            'CREATE TABLE fourtou (uid INTEGER PRIMARY KEY,txt TEXT,date INTEGER)'
            )
            print "Fourtou > DB created"
        return True
    
    def _read(self, data):
        """Read from the database."""
        self.cur.execute('SELECT * FROM fourtou')
        # "fetchone() + yield" doesn't work
        return self.cur.fetchall()
    
    def _write(self, data):
        """Write item into database."""
        assert type(data[0])==str and type(data[1])==int
        self.cur.execute('INSERT INTO fourtou (txt,date) VALUES (?,?)', tuple(data))
        uid = self.cur.lastrowid
        return uid
    
    def _update(self, data):
        """Update database item."""
        assert (type(data[0]) in [int,long]) and type(data[1])==str
        data = (data[1], data[0])
        self.cur.execute('UPDATE fourtou SET txt=? WHERE uid=?', data)
        return True
    
    def _delete(self, data):
        """Delete database item."""
        assert type(data) in [int,long]
        self.cur.execute('DELETE FROM fourtou WHERE uid=?', (data,))
        return True
    
    def _clean(self, data):
        """Clean database."""
        self.cur.execute('VACUUM')
        return True
    
    def close(self):
        """Close database connexion."""
        if self.cnx is not None:
            self.cnx.close()
        self.cnx, self.cur = None, None
        print "Fourtou > DB closed"
        return



#------------------------------------------------------------------------------


class FTIcon(gtk.Image):
    """Status icon."""
    
    def __init__(self, ft):
        gtk.Image.__init__(self)
        self.ft = ft
        s = self.ft.parent.get_size()
        s = min(s) if type(s)==tuple else s
        pixfile = os.path.join(DATADIR, "fourtou.svg")
        pix = gtk.gdk.pixbuf_new_from_file_at_size(pixfile, s, s)
        self.set_from_pixbuf(pix)
        self.set_tooltip_text("Fourtou")
        self.connect('unrealize', self.ev_unrealize)
        # Drag and Drop
        self.drag_dest_set(gtk.DEST_DEFAULT_ALL, [], gtk.gdk.ACTION_COPY)
        self.drag_dest_add_text_targets()
        self.drag_dest_add_uri_targets()
        self.connect('drag_data_received', self.ev_dragreceived)
        # Add to the applet
        self.ft.parent.add_events(gtk.gdk.BUTTON_PRESS_MASK)
        self.ft.parent.connect('button-press-event', self.ev_click)
        self.ft.parent.add(self)
    
    def ev_click(self, w, event):
        """Display the main window."""
        if (event.button == 1) and (event.type == gtk.gdk.BUTTON_PRESS):
            self.ft.win.pop()
            return True
        return False
    
    def ev_dragreceived(self, w, context, x, y, selection, tg_type, timestmp):
        """Get data from Drag-and-Drop."""
        txt = str(selection.data)
        txt = txt.replace('\n', ' ').replace('\t', ' ')
        txt = txt.replace('\r', '').replace('\x00', '')
        txt = txt.strip()
        if len(txt) > 2048:
            return False
        self.ft.win.tree.add_row(txt, self.ft.get_date())
        return True
    
    def ev_unrealize(self, *data):
        """Handle quitting event."""
        self.ft.data.close()
        return



#------------------------------------------------------------------------------


class FTTreeModel(gtk.TreeModelSort):
    """Model for the TreeView."""
    
    def __init__(self, ft, win):
        list = gtk.ListStore(long, str, int)
        flist = list.filter_new()
        gtk.TreeModelSort.__init__(self, flist)
        self.ft = ft
        self.list, self.flist = list, flist
        self.treev = win.gui.get_object('treeview')
        self.filter = win.gui.get_object('entry_filter').get_text
        self.load_list()
        self.flist.set_visible_func(self.is_row_visible)
    
    def load_list(self):
        """Reload list from database."""
        # Deactivate some stuff to improve perfs
        self.treev.freeze_child_notify()
        self.set_sort_column_id(-1, gtk.SORT_ASCENDING)
        # Rebuild the ListStore
        self.list.clear()
        for r in self.ft.data.open('r'):
            self.list.append(r)
        # Reactivate
        self.set_sort_column_id(2, gtk.SORT_DESCENDING)
        self.treev.thaw_child_notify()
        return
    
    def is_row_visible(self, model, iter):
        """Check if the row should be visible."""
        # "model" and "iter" correspond to the TreeModelFilter's child
        # ie. the ListStore
        f = self.filter().lower()
        v = model.get_value(iter, 1)
        if v is None:
            return False
        return False if v.lower().find(f) < 0 else True
    
    def get_selected_iter(self):
        """Return ListStore iter from selection."""
        s = self.treev.get_selection()
        (model, iter1) = s.get_selected()
        if iter1:
            iter2 = model.convert_iter_to_child_iter(None, iter1)
            iter3 = model.get_model().convert_iter_to_child_iter(iter2)
        else:
            iter3 = None
        return iter3
    
    def add_row(self, txt, date):
        """Add a row from text+date."""
        uid = self.ft.data.open('w', (txt,date))
        print "Fourtou > Add:", (uid,txt,date)
        if uid is None:
            msg = "ERROR: impossible to save data!"
            self.ft.send_notif(msg, "dialog-warning", "critical")
        else:
            self.list.append([uid,txt,date])
            self.ft.send_notif(txt, "dialog-information", "low")
        return
    
    def delete_row(self):
        """Delete selected row."""
        iter = self.get_selected_iter()
        if iter:
            uid = self.list.get_value(iter, 0)
            print "Fourtou > Delete:", uid
            if self.ft.data.open('d', uid):
                self.list.remove(iter)
        return
    
    def copy_row(self):
        """Put the selected row text in a clipboard."""
        iter = self.get_selected_iter()
        if iter:
            txt = self.list.get_value(iter, 1)
            self.ft.win.clipb.set_text(txt)
        return
    
    def open_row(self):
        """Try to open selected row as file."""
        iter = self.get_selected_iter()
        if iter:
            txt = self.list.get_value(iter, 1)
            for t in txt.split(' '):
                if t.find("://") > 1:
                    try:
                        print "Fourtou > Open:", t
                        p = subprocess.Popen(["xdg-open",t])
                        p.wait()
                    except:
                        pass
        return
    
    def update_row(self, new_text):
        """Update text in selected row."""
        iter = self.get_selected_iter()
        if iter:
            uid = self.list.get_value(iter, 0)
            print "Fourtou > Edit:", (uid,new_text)
            if self.ft.data.open('u', (uid,new_text)):
                self.list.set_value(iter, 1, new_text)
        return



#------------------------------------------------------------------------------


class FTWindow(gtk.Window):
    """Main window."""
    
    def __init__(self, ft):
        gtk.Window.__init__(self)
        self.ft = ft
        # Window properties
        self.set_transient_for(None)
        self.set_title("Fourtou")
        img, sizes = DATADIR+"fourtou.svg", [16,24,32,48]
        icons = [gtk.gdk.pixbuf_new_from_file_at_size(img,s,s) for s in sizes]
        self.set_icon_list(*icons)
        self.set_default_size(400, 250)
        self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
        self.connect('delete_event', self.ev_deleteevent)
        # Get default clipboard
        self.clipb = gtk.clipboard_get()
        # Build interface
        self.build_gui()
        self.tree = FTTreeModel(self.ft, self)
        self.configure_tv()
        self.actions = {
            'c': self.tree.copy_row, 'Delete': self.tree.delete_row,
            'f': self.gui.get_object('entry_filter').grab_focus,
            'o': self.tree.open_row, 'r': self.tree.load_list}
        self.hide()
    
    def build_gui(self):
        """Load user interface."""
        self.gui = gtk.Builder()
        self.gui.add_from_file(DATADIR + "fourtou.glade")
        self.gui.connect_signals(self)
        c = self.gui.get_object('maincontainer')
        self.add(c)
        c.show_all()
        return
    
    def configure_tv(self):
        """Setup the TreeView."""
        treev = self.gui.get_object('treeview')
        treev.set_model(self.tree)
        treev.set_reorderable(False)
        treev.get_selection().set_mode(gtk.SELECTION_SINGLE)
        # Column 1
        crend = gtk.CellRendererText()
        crend.set_properties(editable=True, ellipsize=pango.ELLIPSIZE_END)
        crend.connect('edited', self.ev_cell_edit)
        c1 = gtk.TreeViewColumn("Text", crend, text=1)
        c1.set_expand(True)
        c1.set_sort_column_id(1)
        treev.append_column(c1)
        # Column 2
        c2 = gtk.TreeViewColumn("Date", gtk.CellRendererText(), text=2)
        c2.set_expand(False)
        c2.set_sort_column_id(2)
        treev.append_column(c2)
        return
    
    def pop(self):
        """Redisplay the window."""
        self.present()
        self.gui.get_object('entry_filter').grab_focus()
        self.gui.get_object('treeview').get_selection().unselect_all()
        return
    
    def ev_cell_edit(self, cell, num, new_text):
        """Handle TreeView cell edit."""
        self.tree.update_row(new_text)
        return
    
    def ev_filterchanged(self, *data):
        """Handle change in filter Entry."""
        # Refilter according to the "self.tree.is_row_visible" function
        self.tree.get_model().refilter()
        return
    
    def ev_keypress(self, w, event):
        """Handle keyboard events in the TreeView."""
        k = gtk.gdk.keyval_name(event.keyval)
        if k in self.actions:
            ctrl = event.state & gtk.gdk.CONTROL_MASK
            if len(k)>1 or (len(k)==1 and ctrl):
                self.actions[k]()
                return True
        return False
    
    def ev_clean(self, *data):
        """Handle click on clean button."""
        self.gui.get_object('entry_filter').set_text("")
        return
    
    def ev_deleteevent(self, *data):
        """Handle 'delete_event' event."""
        self.hide()
        # Return 'False' to quit
        return True



#===# MAIN #===================================================================


if __name__ == '__main__':
    print "Fourtou > START"
    if "--test" in sys.argv:
        win = gtk.Window()
        win.set_transient_for(None)
        win.set_default_size(75, 75)
        win.connect('destroy', gtk.main_quit)
        Fourtou(win)
        gtk.main()
    else:
        import gnomeapplet
        #gnomeapplet.bonobo_factory(
        #    'OAFIID:FourtouApplet_Factory',
        #    gnomeapplet.Applet.__gtype__,
        #    'fourtou',
        #    __version__,
        #    Fourtou
        #)
        gnomeapplet.factory_main("FourtouAppletFactory", gnomeapplet.Applet.__gtype__, Fourtou)
    print "Fourtou > STOP"



#==============================================================================

